package cn.aixuxi.notebank.authc;

import cn.aixuxi.notebank.constant.DefConstant;
import cn.aixuxi.notebank.entity.PermissionEntity;
import cn.aixuxi.notebank.entity.RoleEntity;
import cn.aixuxi.notebank.entity.UserEntity;
import cn.aixuxi.notebank.service.UserService;
import cn.aixuxi.notebank.util.ConverUtils;
import cn.aixuxi.notebank.util.JwtUtils;
import cn.aixuxi.notebank.util.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;

import java.util.LinkedHashSet;
import java.util.Set;

/**
 * 类描述 Shiro Realm
 *
 * @author ruozhuliufeng
 * @version 1.0
 * @date 2020/11/15 21:48
 */
@Component
@Slf4j
public class ShiroRealm extends AuthorizingRealm {

    /**
     * 功能描述: 必须重写此方法，不然Shiro会报错
     * @param token 1
     * @return : boolean
     * @author : ruozhuliufeng
     * @date : 2020/11/15 21:50
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    @Autowired
    UserService userService;

    @Autowired
    @Lazy
    private RedisUtils redisUtils;

    LinkedHashSet hashSet = new LinkedHashSet();

    /**
     * 功能描述: 用户信息认证是在用户进行登录的时候进行验证(不存redis)，也就是说验证用户输入的账号和密码是否正确，
     * 错误则抛出异常
     *
     * @param authenticationToken 用户登录的账号密码信息
     * @return : 封装了用户信息的AuthenticationInfo实例
     * @author : ruozhuliufeng
     * @date : 2020/12/9 9:58
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String token = (String) authenticationToken.getCredentials();
        log.info("获取到的Token信息为：" + authenticationToken.getCredentials());
        if (token == null) {
            log.info("--------身份认证失败-----");
            throw new AuthenticationException("Token为空！");
        }
        // 校验Token有效性
        UserEntity loginUser = this.checkUserTokenIsEffect(token);
        return new SimpleAuthenticationInfo(loginUser, token, getName());
    }


    /**
     * Shiro 的权限授权是通过继承AuthorizingRealm抽象类，重载doGetAuthorizationInfo();
     * 当访问到页面的时候，链接配置了相应的权限或者 Shiro 标签才会执行此方法否则不会执行，
     * 所以如果只是简单的身份认证没有权限的控制的话，那么这个方法可以不进行实现，直接返回 null 即可。
     * 在这个方法中主要是使用类：SimpleAuthorizationInfo进行角色的添加和权限的添加。
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        log.info("========Shiro权限认证开始==========");
        String username = null;
        if (principals != null) {
            UserEntity userEntity = (UserEntity) principals.getPrimaryPrincipal();
            username = userEntity.getUsername();
        }
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        // 设置用户拥有的角色集合，如“admin”，“test“
        Set<String> roleSet = userService.getUserRolesSetByUsername(username);
        info.setRoles(roleSet);
        // 设置用户拥有的权限集合，如“note:add”，“role:delete”
        Set<String> permissionSet = userService.getUserPermissionsSetByUsername(username);
        info.addStringPermissions(permissionSet);
        log.info("=======Shiro权限认证成功=======");
        return info;
    }

    /**
     * 功能描述: 校验Token的有效性
     *
     * @param token Token
     * @return : cn.aixuxi.notebank.entity.UserEntity
     * @author : ruozhuliufeng
     * @date : 2020/12/9 10:07
     */
    private UserEntity checkUserTokenIsEffect(String token) {
        // 解密username,用于和数据库进行对比
        String username = JwtUtils.getUsername(token);
        if (username == null) {
            throw new AuthenticationException("Token非法无效！");
        }
        // 查询用户信息
        log.info("====校验Token是否有效========");
        UserEntity loginUser = userService.findByUsername(username);
        if (loginUser == null) {
            throw new AuthenticationException("用户不存在");
        }
        // 判断用户状态
        if (loginUser.getStatus() != 1) {
            throw new AuthenticationException("账号已被锁定，请联系管管理员！");
        }
        // 校验Token是否超时失效 & 或者账号密码是否错误
        if (!jwtTokenRefresh(token, username, loginUser.getPassword())) {
            throw new AuthenticationException("Token失效，请重新登录");
        }
        return loginUser;
    }

    /**
     * 功能描述: JWTToken刷新生命周期(实现：用户在线操作不掉线功能)
     * 1、登录成功后将用户的JWT生成的Token作为K、V存储到cache缓存里面（这时候K、V值一样），缓存有效期设置为JWT有效时间的2倍
     * 2、当该用户再次请求时，通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证
     * 3、当该用户再次请求jwt生成的Token值已经超时，但该Token对应的cache中的k还是存在，则表示该用户一直在操作，只是JWT的token失效了，程序会给token对应k映射的v值重新生成JWTToken并覆盖V值，重新计算生命周期
     * 4、当该用户再次请求JWT生成的Token值已经超时，并在cache中不存在对应的K，则表示该用户账户空闲超时，返回用户信息已失效，需要重新登录
     *
     * @param token    token
     * @param username 用户名
     * @param password 密码
     * @return : boolean
     * @author : ruozhuliufeng
     * @date : 2020/12/9 11:00
     */
    public boolean jwtTokenRefresh(String token, String username, String password) {
        String cacheToken = String.valueOf(redisUtils.get(DefConstant.PREFIX_USER_TOKEN + token));
        if (ConverUtils.isNotEmpty(cacheToken)) {
            // 校验Token的有效性
            if (!JwtUtils.verify(cacheToken, username, password)) {
                String newAuthorization = JwtUtils.sign(username, password);
                // 设置超时时间
                redisUtils.set(DefConstant.PREFIX_USER_TOKEN + token, newAuthorization);
                redisUtils.expire(DefConstant.PREFIX_USER_TOKEN + token, DefConstant.TOKEN_EXPIRE_TIME);
                log.info("========用户在线操作，更新Token========");
            }
            return true;
        }
        return false;
    }

    /**
     * 功能描述: 清除当前用户的权限认证缓存
     *
     * @param principals 权限信息
     * @return : void
     * @author : ruozhuliufeng
     * @date : 2020/12/9 11:16
     */
    @Override
    protected void clearCachedAuthorizationInfo(PrincipalCollection principals) {
        super.clearCachedAuthorizationInfo(principals);
    }
}
